/* 
 * Copyright 2001-2009 Terracotta, Inc. 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 
 * use this file except in compliance with the License. You may obtain a copy 
 * of the License at 
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *   
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
 * License for the specific language governing permissions and limitations 
 * under the License.
 * 
 */

package org.go.work.self;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.Hashtable;
import java.util.List;

import javax.ejb.EJBHome;
import javax.ejb.EJBMetaData;
import javax.ejb.EJBObject;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;

import org.go.Go;
import org.go.GoContext;
import org.go.expcetion.GoException;
import org.go.expcetion.WorkExecutionException;
import org.go.log.Log;
import org.go.log.LoggerFactory;
import org.go.work.WorkDataMap;

/**
 * <p>
 * A <code>Job</code> that invokes a method on an EJB.
 * </p>
 * 
 * <p>
 * Expects the properties corresponding to the following keys to be in the
 * <code>JobDataMap</code> when it executes:
 * <ul>
 * <li><code>EJB_JNDI_NAME_KEY</code>- the JNDI name (location) of the
 * EJB's home interface.</li>
 * <li><code>EJB_METHOD_KEY</code>- the name of the method to invoke on the
 * EJB.</li>
 * <li><code>EJB_ARGS_KEY</code>- an Object[] of the args to pass to the
 * method (optional, if left out, there are no arguments).</li>
 * <li><code>EJB_ARG_TYPES_KEY</code>- an Class[] of the types of the args to 
 * pass to the method (optional, if left out, the types will be derived by 
 * calling getClass() on each of the arguments).</li>
 * </ul>
 * <br/>
 * The following keys can also be used at need:
 * <ul>
 * <li><code>INITIAL_CONTEXT_FACTORY</code> - the context factory used to 
 * build the context.</li>
 * <li><code>PROVIDER_URL</code> - the name of the environment property
 * for specifying configuration information for the service provider to use.
 * </li>
 * </ul>
 * </p>
 * 
 * <p>
 * The result of the EJB method invocation will be available to 
 * <code>Job/TriggerListener</code>s via
 * <code>{@link org.quartz.JobExecutionContext#getResult()}</code>.
 * </p>
 * 
 * @author hejie
 */
public class EJBInvokerWork implements Go {
	/**
	 * 
	 */
	private static final long serialVersionUID = 8357513525564102157L;

	private static final Log log = LoggerFactory.getLog(EJBInvokerWork.class);

	public static final String CREDENTIALS = "java.naming.security.credentials";

	public static final String EJB_ARG_TYPES_KEY = "argTypes";

	public static final String EJB_ARGS_KEY = "args";

	public static final String EJB_JNDI_NAME_KEY = "ejb";

	public static final String EJB_METHOD_KEY = "method";

	public static final String INITIAL_CONTEXT_FACTORY = "java.naming.factory.initial";

	public static final String PRINCIPAL = "java.naming.security.principal";

	public static final String PROVIDER_URL = "java.naming.provider.url";

	public EJBInvokerWork() {
		// nothing
	}

	private InitialContext getInitialContext(WorkDataMap jobDataMap) throws NamingException {
		Hashtable params = new Hashtable(2);

		String initialContextFactory = jobDataMap.getString(INITIAL_CONTEXT_FACTORY);
		if (initialContextFactory != null) {
			params.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
		}

		String providerUrl = jobDataMap.getString(PROVIDER_URL);
		if (providerUrl != null) {
			params.put(Context.PROVIDER_URL, providerUrl);
		}

		String principal = jobDataMap.getString(PRINCIPAL);
		if (principal != null) {
			params.put(Context.SECURITY_PRINCIPAL, principal);
		}

		String credentials = jobDataMap.getString(CREDENTIALS);
		if (credentials != null) {
			params.put(Context.SECURITY_CREDENTIALS, credentials);
		}

		return (params.size() == 0) ? new InitialContext() : new InitialContext(params);
	}

	@SuppressWarnings("finally")
	@Override
	public Boolean doWork(GoContext context) throws WorkExecutionException {
		log.debug("EJBInvokerWorker doWork start...");

		WorkDataMap dataMap = context.getMergedJobDataMap();
		String ejb = dataMap.getString(EJB_JNDI_NAME_KEY);
		String method = dataMap.getString(EJB_METHOD_KEY);
		Object[] arguments = dataMap.get(EJB_ARGS_KEY);
		if (arguments == null) {
			arguments = new Object[0];
		}

		if (ejb == null) {
			// must specify remote home
			throw new WorkExecutionException();
		}

		InitialContext jndiContext = null;
		// get initial context
		try {
			jndiContext = getInitialContext(dataMap);
		} catch (NamingException ne) {
			throw new WorkExecutionException(ne);
		}

		try {
			Object value = null;

			// locate home interface
			try {
				value = jndiContext.lookup(ejb);
			} catch (NamingException ne) {
				throw new WorkExecutionException(ne);
			}

			// get home interface
			EJBHome ejbHome = (EJBHome) PortableRemoteObject.narrow(value, EJBHome.class);

			// get meta data
			EJBMetaData metaData = null;

			try {
				metaData = ejbHome.getEJBMetaData();
			} catch (RemoteException re) {
				throw new WorkExecutionException(re);
			}

			// get home interface class
			Class homeClass = metaData.getHomeInterfaceClass();

			// get remote interface class
			Class remoteClass = metaData.getRemoteInterfaceClass();

			// get home interface
			ejbHome = (EJBHome) PortableRemoteObject.narrow(ejbHome, homeClass);

			Method methodCreate = null;

			try {
				// create method 'create()' on home interface
				methodCreate = homeClass.getMethod("create", ((Class[]) null));
			} catch (NoSuchMethodException nsme) {
				throw new WorkExecutionException(nsme);
			}

			// create remote object
			EJBObject remoteObj = null;

			try {
				// invoke 'create()' method on home interface
				remoteObj = (EJBObject) methodCreate.invoke(ejbHome, ((Object[]) null));
			} catch (IllegalAccessException iae) {
				throw new WorkExecutionException(iae);
			} catch (InvocationTargetException ite) {
				throw new WorkExecutionException(ite);
			}

			// execute user-specified method on remote object
			Method methodExecute = null;

			try {
				// create method signature

				Class[] argTypes = (Class[]) dataMap.get(EJB_ARG_TYPES_KEY);
				if (argTypes == null) {
					argTypes = new Class[arguments.length];
					for (int i = 0; i < arguments.length; i++) {
						argTypes[i] = arguments[i].getClass();
					}
				}

				// get method on remote object
				methodExecute = remoteClass.getMethod(method, argTypes);
			} catch (NoSuchMethodException nsme) {
				throw new WorkExecutionException(nsme);
			}

			try {
				// invoke user-specified method on remote object
				Object returnObj = methodExecute.invoke(remoteObj, arguments);

				// Return any result in the JobExecutionContext so it will be 
				// available to Job/TriggerListeners
				context.setResult(returnObj);
			} catch (IllegalAccessException iae) {
				throw new WorkExecutionException(iae);
			} catch (InvocationTargetException ite) {
				throw new WorkExecutionException(ite);
			}
		} finally {
			// Don't close jndiContext until after method execution because
			// WebLogic requires context to be open to keep the user credentials
			// available.  See JIRA Issue: QUARTZ-401
			if (jndiContext != null) {
				try {
					jndiContext.close();
				} catch (NamingException e) {
					// Ignore any errors closing the initial context
				}
			}
			log.debug("EJBInvokerWorker doWork end.");
			return true;
		}
	}

	@Override
	public List selectWork(String taskParameter, String ownSign, int taskItemNum, List taskItemList, int eachFetchDataNum) throws GoException {
		// TODO Auto-generated method stub
		return null;
	}
}